﻿namespace AbpPlusPlus.RabbitMq
{
    using System;
    using System.IO;
    using System.Net.Sockets;

    using AbpPlusPlus.RabbitMq.Configuration;

    using Castle.Core.Logging;

    using Polly;
    using RabbitMQ.Client;
    using RabbitMQ.Client.Events;
    using RabbitMQ.Client.Exceptions;

    public class RabbitMqProvider
        : IRabbitMqProvider,IDisposable
    {

        private readonly IAbpRabbitMqModuleConfiguration _rabbitMqConfiguration;

        private readonly IConnectionFactory _connectionFactory;

        private readonly object _sync_root = new object();

        IConnection _connection;
        bool _disposed;

        public RabbitMqProvider(IAbpRabbitMqModuleConfiguration configuration)
        {
            this._rabbitMqConfiguration = configuration;
            this._connectionFactory = new ConnectionFactory {
                                                                    UserName=this._rabbitMqConfiguration.UserName,
                                                                    Password=this._rabbitMqConfiguration.Password,
                                                                    VirtualHost=this._rabbitMqConfiguration.VirtualHost
                                                                };
            this.Logger = NullLogger.Instance;
        }

        public ILogger Logger { get; set; }
        
        public bool IsConnected => this._connection != null && this._connection.IsOpen && !this._disposed;

        public IModel CreateModel()
        {
            if (!this.IsConnected)
            {
                throw new InvalidOperationException("No RabbitMQ connections are available to perform this action");
            }

            return this._connection.CreateModel();
        }

        public void Dispose()
        {
            if (this._disposed) return;

            this._disposed = true;

            try
            {
                this._connection.Dispose();
            }
            catch (IOException ex)
            {
                this.Logger.Error(ex.Message,ex);
            }
        }

        public bool TryConnect()
        {
            this.Logger.Info("RabbitMQ Client is trying to connect");

            lock (this._sync_root)
            {
                var policy = Policy.Handle<SocketException>()
                    .Or<BrokerUnreachableException>()
                    .WaitAndRetry(5, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), (ex, time) =>
                            {
                                this.Logger.Warn(ex.ToString());
                            }
                    );

                policy.Execute(() =>
                    {
                        this._connection = this._connectionFactory
                            .CreateConnection();
                    });

                if (this.IsConnected)
                {
                    this._connection.ConnectionShutdown += this.OnConnectionShutdown;
                    this._connection.CallbackException += this.OnCallbackException;
                    this._connection.ConnectionBlocked += this.OnConnectionBlocked;

                    this.Logger.Info($"RabbitMQ persistent connection acquired a connection {this._connection.Endpoint.HostName} and is subscribed to failure events");

                    return true;
                }
                else
                {
                    this.Logger.Error("FATAL ERROR: RabbitMQ connections could not be created and opened");

                    return false;
                }
            }
        }

        private void OnConnectionBlocked(object sender, ConnectionBlockedEventArgs e)
        {
            if (this._disposed) return;

            this.Logger.Warn("A RabbitMQ connection is shutdown. Trying to re-connect...");

            this.TryConnect();
        }

        void OnCallbackException(object sender, CallbackExceptionEventArgs e)
        {
            if (this._disposed) return;

            this.Logger.Warn("A RabbitMQ connection throw exception. Trying to re-connect...");

            this.TryConnect();
        }

        void OnConnectionShutdown(object sender, ShutdownEventArgs reason)
        {
            if (this._disposed) return;

            this.Logger.Warn("A RabbitMQ connection is on shutdown. Trying to re-connect...");

            this.TryConnect();
        }
    }
}